iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0

RANSAC

雖然我們學到 ORB 能自動找到數十甚至上百對的匹配點,但其中不可避免地會包含一些錯誤的匹配。如果我們把這些包含「雜訊」的匹配點全部丟進去計算單應性矩陣,結果可能相當不好,因此隨機樣本共識 (RANdom SAmple Consensus, RANSAC) 由此誕生。

RANSAC 的核心思想是一種迭代式的投票機制,它的想法是「假設一小部分數據是乾淨的,用它們建立一個模型,然後看看有多少其他數據支持這個模型」。流程基本如下

  1. 隨機採樣 (RANdom SAmple):從所有的匹配點中,隨機選取計算模型所需的最小樣本數量。對於單應性矩陣 H,這個數量是 4 對匹配點。

  2. 計算模型:使用這 4 對點,計算出一個候選的單應性矩陣 H。

  3. 驗證與共識 (consensus):將所有的匹配點,都用這個候選的 H 進行變換,然後計算其與實際對應點之間的誤差(重投影誤差)。如果誤差小於某個閾值,我們就認為這個點是一個「內點」(inliers),即它「支持」或「同意」這個模型。統計出支持這個模型 H 的內點總數。

  4. 迭代:重複步驟 1-3 多次(例如 100 次)。

  5. 選出最佳模型:在所有迭代中,那個獲得最多內點支持的單應性矩陣 H,就被認為是最佳模型。

RANSAC 的優點在於,只要內點的比例足夠高,它就有極大的機率在某次迭代中,恰好選中一組全部由內點構成的樣本,從而找到正確的模型,完全不受外點 (outliers) 的干擾。

全景拼接

全景拼接 (panorama stitching) 就是將多張有重疊區域的圖片,無縫地拼接成一張更寬廣的圖片。有了 ORB 特徵匹配和 RANSAC 穩健估計,我們現在擁有實現它所需的所有工具。

完整的處理流程如下

  1. 讀取兩張有重疊區域的圖片(例如,一張偏左,一張偏右)。

  2. 使用 ORB 在兩張圖片中分別偵測特徵點並計算描述子。

  3. 使用 Brute-Force 匹配器找到兩組描述子之間的對應關係。

  4. 將匹配好的點對座標傳入 cv2.findHomography 函式。這個函式內部就實現了 RANSAC,它會自動從大量匹配中,穩健地計算出最佳的單應性矩陣 H。

  5. 有了矩陣 H,我們使用 cv2.warpPerspective 函式,將其中一張圖片進行透視變換,使其「扭曲」到與另一張圖片相同的視角平面上。

  6. 最後,建立一個足夠大的畫布,將兩張圖片(一張是原始的,一張是變換後的)拼接在一起。

import cv2
import numpy as np

def stitch_images(img1, img2, min_match_count=10):
    """
    使用 ORB 和 RANSAC 拼接兩張圖片。
    img1: 右側圖片
    img2: 左側圖片
    """
    # --- 1. 偵測特徵並匹配 ---
    orb = cv2.ORB_create(nfeatures=2000)
    kp1, des1 = orb.detectAndCompute(img1, None)
    kp2, des2 = orb.detectAndCompute(img2, None)

    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
    matches = bf.match(des1, des2)
    matches = sorted(matches, key=lambda x: x.distance)
    
    # --- 2. 使用 RANSAC 計算單應性矩陣 ---
    if len(matches) > min_match_count:
        # 提取匹配點的座標
        src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
        dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)

        # cv2.findHomography(src, dst, method, ransacReprojThreshold)
        # method=cv2.RANSAC: 指定使用 RANSAC 演算法
        # ransacReprojThreshold: 重投影誤差閾值,通常設為 4.0 或 5.0
        # M: 計算出的 3x3 單應性矩陣
        # mask: 標記了內點(1)和外點(0)
        M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
        
        # --- 3. 應用透視變換 ---
        h1, w1 = img1.shape[:2]
        h2, w2 = img2.shape[:2]

        # 將 img1 進行變換,使其與 img2 對齊
        # 輸出畫布的大小設定為兩張圖的總寬度和較高的那個高度
        warped_img = cv2.warpPerspective(img1, M, (w1 + w2, max(h1, h2)))
        
        # --- 4. 拼接圖片 ---
        # 將 img2 (左側圖) 放置在畫布的左側
        # 然後將變換後的 img1 (右側圖) 疊加在上面
        result = warped_img.copy()
        result[0:h2, 0:w2] = img2
        
        # (可選) 移除右側的黑色邊界
        # 找到第一個非黑色的列
        gray_result = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
        _, thresh = cv2.threshold(gray_result, 1, 255, cv2.THRESH_BINARY)
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if contours:
            cnt = max(contours, key=cv2.contourArea)
            x, y, w, h = cv2.boundingRect(cnt)
            result = result[y:y+h, x:x+w]
        
        return result
    else:
        print(f"Not enough matches are found - {len(matches)}/{min_match_count}")
        return None

# --- 主程式 ---
if __name__ == '__main__':
    # 讀取圖片 (img_right 是右邊的圖, img_left 是左邊的圖)
    img_right = cv2.imread('panorama2.jpg')
    img_left = cv2.imread('panorama1.jpg')

    if img_right is None or img_left is None:
        print("圖片讀取失敗!請檢查路徑。")
    else:
        stitched_result = stitch_images(img_right, img_left)
        if stitched_result is not None:
            cv2.imshow('Stitched Panorama', stitched_result)
            cv2.waitKey(0)
            cv2.imwrite("panorama.jpg", stitched_result)
            cv2.destroyAllWindows()

原圖
https://ithelp.ithome.com.tw/upload/images/20250819/201781005Ll9gXR3oo.jpghttps://ithelp.ithome.com.tw/upload/images/20250819/20178100oFj6TJRc2W.jpg
合成後
https://ithelp.ithome.com.tw/upload/images/20250819/20178100yYhcRbudO6.jpg


上一篇
Day 8 - 從匹配到變換
下一篇
Day 10 - 機器學習初探(一) KNN
系列文
從0開始:傳統圖像處理到深度學習模型23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言